java快速学习速查(6)[进阶篇]
java快速学习速查(6)[进阶篇]
通过了一段时间的学习,也是终于得开个新的文章了,前面几篇都超级冗杂的,这篇是新写的,不会太复杂
文件(file)
java.io.File 类表示一个普通文件,也可以是目录,File可以进行文件目录的创建、删除改、查询目录下面的文件。但是它不涉及到任何文件的读写操作。
文件分隔符
原因是\在java里面有特殊的意义,使用它需要转义也就是在前面加一个反斜杠 \ 。1
String paht="c:\\a.txt";
构造方法是:1
2
3
4
5
6
7
8File(String pathname)//通过将给定路径名字符串转换为抽象路径名来创建一个新File实例。
public class Test1 {
public static void main(String[] args) {
// 文件路径名
File file =new File("D:\\a.txt");
}
}
注意喔
1.一个File对象代表的是硬盘中实际存在的一个文件或者目录。
2.无论该路径下是否存在文件或者目录,都不影响File对象的创建。
常用方法摘要
接下来的这些是在java中的涉及文件的方法蓝图,我尽可能整理出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 //返回file的绝对路径字符串
public String getAbsolutePath();
//返回由此抽象路径名表示的 文件 的名称。
public String getName();
//返回由此抽象路径名表示的文件的长度。
public long length();
//返回转换为路径名字字符串
public String toString();
//返回转换为路径名字字符串
public String getPath();
//----------------------------------------------------------------------
//判断功能的方法
//判断File是不是目录
public boolean isDirectory();
//判断File是不是文件
public boolean isFile();
//判断File是不是隐藏文件
public boolean isHidden();
//判断File文件或者目录是否存在
public boolean exists();
//------------------------------------------------------------------------
//创建删除的方法
//创建文件的方法
public boolean createNewFile() throws IOException;
//创建目录的方法
public boolean mkdir();
//创建多级目录的方法
public boolean mkdirs();
//删除文件的方法
public boolean delete();
//删除目录的方法
public boolean delete();
//删除多级目录的方法
public boolean delete();
//------------------------------------------------------------------------
//目录的遍历
//返回目录下的文件或者目录的名称字符串数组
public String[] list();
//返回目录下的文件或者目录的File数组
public File[] listFiles();
若是想通过终端,使用指令完成上述操作见下方指令
1 | cd .. |
指令使用可以很灵活,也很快速。以下是常用整合指令1
#暂时施工中-----------------
绝对路径和相对路径
绝对路径:从盘符开始的路径,这是一个完整的路径。
相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。1
2
3
4
5
6
7//d盘下的某个文件
File f=new File("d:\\a.txt");
System.out.println(f.getAbsolutePath());
// 项目下new.txt
f=new File("new.txt");
System.out.println(f.getAbsolutePath());
判断功能的方法
- public boolean isDirectory():判断File是不是目录
- public boolean isFile():判断File是不是文件
- public boolean exists():判断File文件或目录是否存在
示例部分:1
2
3
4
5
6 //d盘下的某个文件
File f=new File("d:\\a.txt");
// 判断是否存在
System.out.println("d:\\a.txt是否存在"+f.exists());
System.out.println(f.isFile()); // 判断是不是文件
System.out.println(f.isDirectory()); // 判断是不是目录
创建删除的方法
- public boolean createNewFile() throws IOException:创建文件的方法
- public boolean mkdir():创建目录的方法
- public boolean mkdirs():创建多级目录的方法
- public boolean delete():删除文件或者目录的方法
注意事项:
1.mkdir
和mkdirs
方法的返回值都是boolean类型,返回true表示创建成功,返回false表示创建失败。
2.delete
方法的返回值也是boolean类型,返回true表示删除成功,返回false表示删除失败。
3.delete
方法删除目录时,只能删除空目录。
4.delete
方法删除文件时,只能删除文件,不能删除目录。
5.delete
方法删除文件或目录时,必须保证文件或目录存在。
6.delete
方法删除文件或目录时,必须保证文件或目录不是只读的。
7.delete
方法删除文件或目录时,必须保证文件或目录不是正在被使用的。
示例部分:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 创建一个目录
File dir = new File("d:\\demo");
boolean mkdir = dir.mkdir();
System.out.println(mkdir); // true
// 创建多个目录
File dirs = new File("d:\\demo\\a\\b\\c");
boolean mkdirs = dirs.mkdirs();
System.out.println(mkdirs); // true
// 创建一个文件
File file = new File("d:\\demo\\a.txt");
boolean newFile = file.createNewFile();
System.out.println(newFile); // true
// 删除文件
boolean delete = file.delete();
System.out.println(delete); // true
// 删除目录
boolean delete1 = dir.delete();
System.out.println(delete1); // true
boolean delete2 = dirs.delete();
System.out.println(delete2); // true
目录的遍历
- public String[] list():返回目录下的文件或者目录的名称字符串数组
- public File[] listFiles():返回目录下的文件或者目录的File数组
示例部分:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 public class Test4 {
public static void main(String[] args) {
File file=new File("d:\\soft");
// 获取当前目录下的文件以及文件夹的名称
String[] names = file.list();
for(String name :names){
System.out.println(name);
}
System.out.println("~~~~~~~~~~~~~~~");
// 获得当前目录下的文件以及文件夹对象
File[] files = file.listFiles();
for(File f: files){
if(f.isDirectory()){
System.out.println("文件夹-->"+f.getName());
}else{
System.out.println("文件-->"+f.getName());
}
}
}
}
使用递归浏览文件夹信息
递归:从编程角度来看,递归行为可以视为是方法定义中调用自身方法的行为
典型使用案例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 public class Test5 {
// 多级目录打印,到底有多少级目录不知道
public static void main(String[] args) {
//定位到一个文件夹
File fileDir=new File("d:\\soft");
getAllFile(fileDir);
}
// 打印目录下面的目录与文件的信息
public static void getAllFile(File fileDir){
// 获得指定File文件目录下的所有的文件和文件夹
File[] files = fileDir.listFiles();
// 判断文件夹是否为空
if(files!=null){
// 循环遍历,得到每一个File对象
for(File file: files){
// 判断File是文件还是文件夹
if(file.isDirectory()){
// 说明是文件夹
System.out.println("文件夹--->"+file.getName());
getAllFile(file);
}else{
// 如果是文件,打印文件名
// System.out.println("文件--->"+file.getName());
}
}
}
}
}
解析以上代码:可以发现运行逻辑是:
1.首先定位到一个文件夹
2.获得指定File文件目录下的所有的文件和文件夹
3.判断文件夹是否为空
4.如果不为空,循环遍历,得到每一个File对象
5.判断File是文件还是文件夹
6.如果是文件夹,打印文件夹名,并递归调用
7.如果是文件,打印文件名
在此案例上精进(文件搜索,搜索目录中以.java结尾的文件。)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 public class Test6 {
public static void main(String[] args) {
// 查找到某个盘符下所有的.java文件
// 1.搜索目录,无法判断多少级子目录,所以使用递归,遍历所有目录。
// 2.遍历目录时,获取子文件,通过文件名称,判断是不是以.java结尾的。
// 定位到一个指定的目录
File fileDir =new File("C:\\Users\\jiannaize\\IdeaProjects");
// 递归调用打印目录的方法
printDir(fileDir);
}
public static void printDir(File fileDir){
//获取子文件和目录
File[] files = fileDir.listFiles();
// 遍历所有的File类
for(File file : files){
// 判断是不是文件
if(file.isFile()){
// 如果是文件
if(file.getName().endsWith(".java")){
System.out.println("文件名--->"+file.getAbsolutePath());
}
}else{// 否则是文件夹
printDir(file);
}
}
}
}
IO流
程序中的数据需要移动传输,外部的数据输入到计算机,计算机内部的数据输出到尾外部,这个过程的实现就行由io流来完成的,即负责进行数据传输的技术就是IO流
- 输入也叫做读取数据
- 输出也叫做写出数据
输入:可以让程序从外部系统获得数据,例如:读取磁盘,网络,数据库等媒介的数据到程序
输出:可以将程序中的数据输出到外部系统,例如:将数据写到磁盘,网络,数据库等媒介中。
IO流的分类
按照流向分:
- 输入流:可以从外部系统获得数据到程序中
输入input:读取外部数据到程序中,以InputStream,reader结尾 - 输出流:可以将程序中的数据输出到外部系统
输出output:将程序中的数据输出到外部系统,以FileOutputStream,FileWriter结尾
- 输入流:可以从外部系统获得数据到程序中
按照数据类型分:
- 字节流:可以读写任意类型的文件,以字节为单位操作数据
字节输入流:InputStream结尾
字节输出流:OutputStream结尾 - 字符流:只能读写文本文件,以字符为单位操作数据(这玩意底层还是字节流)
字符输入流:Reader结尾
字符输出流:Writer结尾
- 字节流:可以读写任意类型的文件,以字节为单位操作数据
字节流
字节输出流OutputStream
OutputStream抽象类表示字节流所有类的超类,将指定的字节数据写出到目的地,主要方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28//关闭输出流,并释放资源
public void close() throws IOException {
flush();
sync();
}
//将B.length个字节从指定的byte数组写入此输出流。
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
//将指定的字节数组写入len个字节,从偏移量off开始输出。
public void write(byte[] b, int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
flush();
//将指定的字节写入输出流。
public void write(int b) throws IOException {
write(new byte[]{(byte) b}, 0, 1);
}
注意
close方法:关闭输出流,并释放资源,但是在关闭前,会先调用flush方法刷新缓冲区。
FileOutputStream类
FileOutputStream类是文件输出流,用于将数据写入File或FileDescriptor。
构造方法:
- public FileOutputStream(File file):创建文件输出流,以写入由指定的File对象表示的文件。
- public FileOutputStream(String name):创建文件输出流,以写入具有指定名称的文件。
字节输入流InputStream
InputStream抽象类表示字节输入流所有的超类,可以读取字节信息到内存中
方法摘要1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 关闭输入流并释放资源
public void close() throws IOException {
sync();
}
// 从输入流中读取数据的下一个字节
public abstract int read() throws IOException;
// 从输入流中读取一定数量的字节,并将其存储在缓冲区数组b中
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
// 跳过并丢弃输入流中的数据字节
public long skip(long n) throws IOException {
long remaining = count - pos;
if (n <= remaining) {
pos += n;
return n;
}
pos = count;
return remaining;
}
构造方法:
- public FileInputStream(File file):通过打开与实际文件的连接来创建文件输入流,该文件由文件系统中的File对象file命名。
- public FileInputStream(String name):通过打开与实际文件的连接来创建文件输入流,该文件由文件系统中的路径名name命名。
常用方法: - public int read():从输入流中读取数据的下一个字节。
- public int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在缓冲区数组b中。
- public int available():返回输入流中可以读取的字节数。
- public void close():关闭输入流并释放资源。
- public long skip(long n):跳过并丢弃输入流中的数据字节。
read方法:
- public int read():从输入流中读取数据的下一个字节。
- public int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在缓冲区数组b中。
- public int available():返回输入流中可以读取的字节数。
- public void close():关闭输入流并释放资源。
- public long skip(long n):跳过并丢弃输入流中的数据字节。
读取过程中我们一般使用循环去读取该文件内的内容,当返回值为-1时,表示读取到文件的末尾,停止读取,具体代码如下:
示例:1
2
3
4
5
6
7
8
9
10
11
12//低效代码,一个一个遍历
public class text4 {
public static void main(String[] args) throws Exception{
FileInputStream fis = new FileInputStream("new.txt");
int b;
while ((b = fis.read())!= -1){
System.out.println((char) b);
}
}
}
1 | //高效手段:使用字节数组 |
注意
流的关闭原则,先开后关,后开先关
字符流
字符输入流reader
reader抽象类表示字符流所有类的超类,可以读取字符串信息到内存中,主要方法如下:1
2
3
4
5
6
7
8
9
10
11
12// 关闭流并释放资源
public void close() throws IOException {
sync();
}
// 读取单个字符
public int read() throws IOException {
return read(new char[1], 0, 1);
}
// 读取字符数组
public int read(char[] cbuf) throws IOException {
return read(cbuf, 0, cbuf.length);
}
构造方法:
- public FileReader(File file):创建一个新的FileReader对象,该对象使用给定的File对象读取文件。
- public FileReader(String fileName):创建一个新的FileReader对象,该对象使用给定的文件名读取文件。
常用方法:
- public int read():读取单个字符,会提升为int类型。
- public int read(char[] cbuf):将字符读入数组。
- public void close():关闭流并释放资源。
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//经典字符数组读取实例
public class FileReaderDemo {
public static void main(String[] args) throws IOException {
// 创建文件字符输入流
FileReader fr = new FileReader("test.txt");
// 读取文件内容
int len;
// 定义字符数组,存储每次读取到的多个字符
char[] chars = new char[1024];
while ((len = fr.read(chars)) != -1) {
System.out.println(new String(chars, 0, len));
}
// 关闭流
fr.close();
}
}
字符输出流Writer
FileWriter类在关闭资源时候,与FileInputStream类的close方法不同,这是个带缓冲的对象,如果不关闭资源,文件根本没有写入磁盘,而是缓冲区满了之后在一次性写入,所以一定注意释放,这个操作是为了提供性能。
IO操作如果频繁的进行磁盘操作会很影响性能,所以才引入缓冲区
Writer抽象类表示字符输出流所有类的超类,将字符信息写入文件中,主要方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13// 关闭流并释放资源
public void close() throws IOException {
flush();
sync();
}
// 写入字符数组
public void write(char[] cbuf) throws IOException {
write(cbuf, 0, cbuf.length);
}
// 刷新流
public void flush() throws IOException {
sync();
}
构造方法:
- public FileWriter(File file):创建一个新的FileWriter对象,该对象使用给定的File对象写入文件。
- public FileWriter(String fileName):创建一个新的FileWriter对象,该对象使用给定的文件名写入文件。
- public FileWriter(String fileName, boolean append):创建一个新的FileWriter对象,该对象使用给定的文件名写入文件,append参数指定是否追加写入。
因为内置
常用方法:
- public void write(char[] cbuf):写入字符数组。
- public void flush():刷新流。
- public void close():关闭流并释放资源。
示例:如果要追加写入,只需要在构造方法中加入true参数即可。1
2
3
4
5
6
7
8
9
10
11
12
13//高效字符数组写入实例
public class FileWriterDemo {
public static void main(String[] args) throws IOException {
// 创建文件字符输出流
FileWriter fw = new FileWriter("test.txt");
// 写入文件内容
fw.write("hello world");
// 刷新流
fw.flush();
// 关闭流
fw.close();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13//追加写入实例
public class FileWriterDemo {
public static void main(String[] args) throws IOException {
// 创建文件字符输出流
FileWriter fw = new FileWriter("test.txt",true);
// 写入文件内容
fw.write("hello world");
// 刷新流
fw.flush();
// 关闭流
fw.close();
}
}缓冲流
字节缓冲流
字节缓冲流可以大幅度提高读写效率,缓冲流会把数据加入缓存,缓存满后再一次性读到程序或者写入目的地,在IO操作中,请务必添加使用,主要方法如下:1
2
3
4
5
6
7
8
9// 刷新流
public void flush() throws IOException {
sync();
}
// 关闭流并释放资源
public void close() throws IOException {
flush();
sync();
}
构造方法:
- public BufferedInputStream(InputStream in):创建一个新的BufferedInputStream对象,该对象使用给定的InputStream对象读取数据。
- public BufferedOutputStream(OutputStream out):创建一个新的BufferedOutputStream对象,该对象使用给定的OutputStream对象写入数据。
高效字节缓冲流实例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37//高效字节缓冲流实例
public class BufferedStreamDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
//记录开始时间
BufferedInputStream bis =null;
try {
bis = new BufferedInputStream(
new FileInputStream("假装自己是个大文件"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 读取文件内容
byte[] bytes = new byte[1024*8];
while (true) {
int len = 0;
try {
if (!((len = bis.read(bytes)) != -1)) break;
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(new String(bytes, 0, len));
}
// 关闭输入流
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
//记录结束时间
System.out.println(end-start);
}
}
若是遇到权限问题,会弹出
拒绝访问
字样,想要解决这个问题,需要在创建流的时候加入路径,而不是直接写文件名,这样就可以避免权限问题。
字符缓冲流
字符缓冲流可以大幅度提高读写效率,缓冲流会把数据加入缓存,缓存满后再一次性读到程序或者写入目的地,在IO操作中,请务必添加使用,主要方法如下:1
2
3
4
5
6
7
8
9// 刷新流
public void flush() throws IOException {
flushBuffer();
}
// 关闭流并释放资源
public void close() throws IOException {
flush();
super.close();
}
构造方法:
- public BufferedReader(Reader in):创建一个新的BufferedReader对象,该对象使用给定的Reader对象读取数据。
- public BufferedWriter(Writer out):创建一个新的BufferedWriter对象,该对象使用给定的Writer对象写入数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26//高效字符缓冲流实例
public static void main(String[] args) {
try {
//创建流对象
BufferedReader br = new BufferedReader(new FileReader("假装这是个大文件"));
//定义字符串,保存读取一行文字
String line =null;
//循环读数,读到最后返回null
while (true){
try {
if (!(line = br.readLine()!= null)) break;
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(line);
}
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}1
2
3
4
5
6
7
8
9//高效字符缓冲流实例
public static void main(String[] args) throws Exception{
BufferedWriter bw =new BufferedWriter(new FileWriter("time.txt"));
bw.write("我也不知道,先说句话");
bw.newLine();
bw.newLine();
}序列化和反序列化
序列化:将对象以字节的形式写入文件,方便下次读取。
反序列化:将文件中的字节数据读取出来,恢复成对象。
序列化
构造方法:
- public ObjectOutputStream(OutputStream out):创建一个新的ObjectOutputStream对象,该对象使用给定的OutputStream对象写入数据。
常用方法: - public final void writeObject(Object x) throws IOException:将指定的对象写入ObjectOutputStream。
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//序列化实例
public class SerializeDemo {
public static void main(String[] args) throws IOException {
// 创建文件字符输出流
FileOutputStream fos = new FileOutputStream("test.txt");
// 创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(fos);
// 写入对象
oos.writeObject(new Person("张三", 18));
// 刷新流
oos.flush();
// 关闭流
oos.close();
}
}
反序列化
构造方法:
- public ObjectInputStream(InputStream in):创建一个新的ObjectInputStream对象,该对象使用给定的InputStream对象读取数据。
常用方法: - public final Object readObject() throws IOException, ClassNotFoundException:从ObjectInputStream读取一个对象。
示例:1
2
3
4
5
6
7
8
9
10
11
12
13//反序列化实例
public class DeserializeDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 创建文件字符输入流
FileInputStream fis = new FileInputStream("test.txt");
// 创建对象输入流
ObjectInputStream ois = new ObjectInputStream(fis);
// 读取对象
Person p = (Person) ois.readObject();
// 关闭流
ois.close();
}
}objectoutputstream和objectinputstream
在java中,实现序列化的方式是通过让类实现java.io.Serializable接口,这个接口是一个标记接口(没有任何方法),其作用告诉JVM该类的对象可以被序列化,不实现接口的类会抛出NotSerializableException。
ObjectOutputStream
1 | //接口书写 |
1 | public class TestPerson { |
ObjectInputStream
ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package com.iweb.test;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class TestPerson1 {
public static void main(String[] args) throws Exception {
ObjectInputStream ois=new ObjectInputStream(
new FileInputStream("person.txt"));
// 读取一个对象
Person p=(Person) ois.readObject();
System.out.println(p.getName()+"\t"+p.getAge());
// 释放资源
ois.close();
}
}
打印流(PrintStream)
这是个输出信息的类,可以打印任何的数据的信息,一般我们打印的信息都是在控制台输出的,而控制台就是System.out
,PrintStream
类能够方便地打印各种数据类型的值,是一种便捷的输出方式。
字节打印流
构造方法:
- public PrintStream(String fileName):创建一个新的PrintStream对象,该对象使用给定的文件名写入数据。
- public PrintStream(File file):创建一个新的PrintStream对象,该对象使用给定的File对象写入数据。
- public PrintStream(OutputStream out):创建一个新的PrintStream对象,该对象使用给定的OutputStream对象写入数据。
常用方法: - public void print(String str):打印字符串。
- public void println(String str):打印字符串并换行。
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14 public class Test8 {
public static void main(String[] args) {
System.out.println("78");
//创建打印流
//设定系统的输出流向
try {
System.setOut(new PrintStream("out.txt"));
System.out.println("emem");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
字符打印流
构造方法:
- public PrintWriter(String fileName):创建一个新的PrintWriter对象,该对象使用给定的文件名写入数据。
- public PrintWriter(File file):创建一个新的PrintWriter对象,该对象使用给定的File对象写入数据。
- public PrintWriter(OutputStream out):创建一个新的PrintWriter对象,该对象使用给定的OutputStream对象写入数据。
常用方法: - public void print(String str):打印字符串。
- public void println(String str):打印字符串并换行。
示例:1
2
3
4
5
6public static void main(String[] args) throws Exception{
PrintWriter pw = new PrintWriter(new FileWriter("a.txt"));
pw.println("阿巴阿巴");
pw.print("阿巴阿巴也");
pw.close();
}
java设计模式
设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格以及解决问题的思考方式。设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱。
设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
装饰者模式
通过对对象包装,来增强或者修改对象的功能,装饰者模式允许在不改变原有类的情况下,动态的给对象添加新的功能,这种方式不仅提供了代码的 复用性,还使得更加的灵活
javaIO使用者模式可以在不改变原有类的基础上,动态扩展功能
但是这玩意也有很大的缺点,最大的问题是装饰者模式会增加许多小的类,过度使用会使程序变得很复杂。
java网络编程
以
TCP通信程序
TCP 是一种面向连接的协议,它提供可靠的双向数据传输服务
两端通信步骤
- 客户端向服务器端发送连接请求,请求建立连接。
- 服务器端接收客户端的连接请求,并同意连接。
- 客户端和服务器端完成连接后,就可以进行数据的传输了。
- 数据传输完成后,客户端和服务器端可以断开连接。
客户端主动连接服务端,连接成功才能通信,服务端不可以主动去连接客户端。
面向连接
在数据传输前需要建立连接(三次握手),传输结束后断开连接(四次挥手)。
针对java中的实现的话
服务端:java.net.ServerSocket类表示创建ServerSocket对象,相当于开启了一个服务,并等待客户端连接
客户端:java.net.Socket类表示创建Socket对象,相当于请求连接服务端
ServerSocket
ServerSocket类的主要作用是监听客户端的连接请求,当客户端连接时,会返回一个Socket对象,用于与客户端进行数据的交互。
构造方法
- public ServerSocket(int port):创建一个ServerSocket对象,指定监听的端口号。
- public ServerSocket(int port, int backlog):创建一个ServerSocket对象,指定监听的端口号和连接请求队列的长度。
常用方法
- public Socket accept():监听并接收客户端的连接请求,返回一个Socket对象。
- public void close():关闭ServerSocket对象,释放资源。
示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class ServerSocketDemo {
public static void main(String[] args) throws IOException {
// 创建ServerSocket对象,指定监听的端口号
ServerSocket serverSocket = new ServerSocket(8888);
// 监听并接收客户端的连接请求
Socket socket = serverSocket.accept();
// 从Socket对象中获取输入流
InputStream inputStream = socket.getInputStream();
// 读取数据
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
// 打印数据
System.out.println(new String(bytes, 0, len));
// 关闭资源
socket.close();
serverSocket.close();
}
}
Socket
Socket类的主要作用是与服务器端进行数据的交互。Socket类用于实现TCP客户端程序。
构造方法
- public Socket(String host, int port):创建一个Socket对象,指定连接的服务器端的IP地址和端口号。
- public Socket(InetAddress address, int port):创建一个Socket对象,指定连接的服务器端的IP地址和端口号。
常用方法
- public InputStream getInputStream():返回Socket对象的输入流,用于接收服务器端发送的数据。
- public OutputStream getOutputStream():返回Socket对象的输出流,用于向服务器端发送数据。
- public void close():关闭Socket对象,释放资源。
示例1
2
3
4
5
6
7
8
9
10
11
12
13public class SocketDemo {
public static void main(String[] args) throws IOException {
// 创建Socket对象,指定连接的服务器端的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);
// 从Socket对象中获取输出流
OutputStream outputStream = socket.getOutputStream();
// 向服务器端发送数据
outputStream.write("Hello, Server!".getBytes());
// 关闭资源
socket.close();
}
}
第二示例:服务端1
2
3
4
5
6
7
8
9
10
11
12// 服务端
public class ServerTcp {
public static void main(String[] args) throws Exception{
System.out.println("服务端启动,等待客户端连接....");
// 创建指定端口为8888的服务端ServerSocket对象
ServerSocket ss=new ServerSocket(8888);
// 调用ServerSocket.accept() 方法开始接受数据
Socket accept = ss.accept();
System.out.println("客户端连接成功,开始通信...");
}
}
在上述的代码中,我们创建了一个ServerSocket对象,并通过accept()方法等待客户端的连接请求,一旦有了客户端连接成功,就会创建Socket对象,通过这个Socket对象,服务端和客户端可以进行数据传输
第二示例:客户端1
2
3
4
5
6
7
8// 客户端
public class ClientTcp {
public static void main(String[] args) throws Exception{
// 创建一个Socket并连接指定的服务端
// InetAddress.getLocalHost() 本机地址
Socket client=new Socket(InetAddress.getLocalHost(),8888);
}
}
在客户端代码中,创建了Socket对象,指定服务器的地址和端口继续连接,一旦连接成功,就可以在这个Socket上进行数据的发送和接收
简单的一个TCP网络程序
通过这个实例来了解Socket和ServerSocket的使用:
思路如下
- 服务端创建ServerSocket对象,指定监听的端口号。
- 客户端创建Socket对象,指定连接的服务器端的IP地址和端口号。
- 服务端调用accept方法监听并接收客户端的连接请求,返回一个Socket对象。
- 客户端调用getInputStream方法获取输入流,用于接收服务器端发送的数据。
- 客户端调用getOutputStream方法获取输出流,用于向服务器端发送数据。
- 服务端调用getInputStream方法获取输入流,用于接收客户端发送的数据。
- 服务端调用getOutputStream方法获取输出流,用于向客户端发送数据。
客户端和服务端分开写,代码如下:
服务端代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class ServerSocketDemo {
public static void main(String[] args) throws IOException {
// 创建ServerSocket对象,指定监听的端口号
ServerSocket serverSocket = new ServerSocket(8888);
// 监听并接收客户端的连接请求
Socket socket = serverSocket.accept();
// 从Socket对象中获取输入流
InputStream inputStream = socket.getInputStream();
// 读取数据
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
// 打印数据
System.out.println(new String(bytes, 0, len));
// 关闭资源
socket.close();
serverSocket.close();
}
}
客户端代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class SocketDemo {
public static void main(String[] args) throws IOException {
// 创建Socket对象,指定连接的服务器端的IP地址和端口号
// 127.0.0.1 是本地回环地址,用于测试网络连接
//也可以使用InetAddress.getLocalHost()
Socket socket = new Socket("127.0.0.1", 8888);
// 从Socket对象中获取输出流
OutputStream outputStream = socket.getOutputStream();
// 向服务器端发送数据
outputStream.write("Hello, Server!".getBytes());
// 从Socket对象中获取输入流
InputStream inputStream = socket.getInputStream();
// 读取数据
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
// 打印数据
System.out.println(new String(bytes, 0, len));
// 关闭资源
socket.close();
serverSocket.close();
}
}
在客户端代码中,创建了Socket对象,指定连接的服务器端的IP地址和端口号。连接成功后就可以通过Socket对象的输入流和输出流与服务器端进行数据的交互。
在服务端代码中,创建了ServerSocket对象,指定监听的端口号。调用accept方法监听并接收客户端的连接请求,返回一个Socket对象。通过Socket对象的输入流和输出流与客户端进行数据的交互。
注意事项
- 客户端和服务端的IP地址和端口号必须一致,否则连接失败。
- 客户端和服务端必须在同一个网络中,否则连接失败。
第二示例:服务端和客户端通信
1 | package com.iweb.test; |
1 | package com.iweb.test; |
在这两端代码中,服务器通过BufferedReader
读取客户端发送的数据,通过PrintWriter
向客户端发送响应,客户端则响应,先发送数据再接受响应。
面试题:TCP与UPD的区别?
TCP协议:
1.TCP是面向连接的协议,意味着在数据传输开始前,必须先建立连接(通过三次握手),数据传输结束还需要断开连接(通过四次挥手)。
2.TCP可靠的数据传输。
3.TCP由于要进行建立和维护,并且需要确认应答,因此传输速度相对比较慢。
UDP协议:
1.UDP是无连接的协议,不需要连接连接,数据可以直接发送。
2.UDP不可靠的数据传输。
3.UDP因为无连接且没有确认应答,传输速度相对比较快。
TCP协议
OSI七层模型
OSI七层模型是国际标准化组织(ISO)提出的一个网络通信模型,从上到下依次分为(以OSI和TCP/IP的区别表格展现):
没有图啦,三次握手和四次挥手的原理图都请见外站,本站暂时不留存图片(图床故障)
多线程
进程
进程:是指操作系统中的一个独立运行的程序。在windons系统,wx、QQ、IDEA都是一个进程,在window操作系统中任务管理中,可以看到当前操作系统正在运行的进程信息。
进程,也称为任务,所有支持多个进程同时执行的操作系统称做多进程操作系统或多任务操作系统,现在主流的操作系统属于这种类型。
多进程的实现原理:
CPU采用的原理是分时执行,每个进程依次获得一个时间片进入CPU进行执行,在该时间篇执行完以后,该进程保存自身状态,退出CPU,然后其他的进程进入CPU继续执行,由于时间片的时间很短,计算机用户看来程序是同时执行的,而实际上执行方式是穿插依次执行的。
因此操作系统通过快速的在不同进程之间进行切换,因此CPU的执行速度是非常快的,CPU循环依次执行各个程序,以达到给用户造成一种多个进程同时运行的错觉。
进程特点:
进程和进程之间的内部数据和状态都是互相独立的,进程之间不能直接通信或者通信代价很高。进程相对来说是特别消耗系统性能的,比如CPU资源以及内存。
线程
进程的概念相对比较大,而且需要称为一个独立的程序,这样对于变成来说比较麻烦,所以在程序开发中设计了另外一个概念“线程”。
一个进程是由多个线程组成的,他们是包含关系。比如某个程序运行时需要执行1000行代码,CPU需要将这些代码分给多个线程来执行,因为线程才是CPU的最小调度单位,当这些线程执行完毕之后,那么我们的进程也就随着执行完毕了。
线程:是指进程中每个单独、单独执行的流程。Java语言支持在一个程序内部同时执行多个流程,其中每个单独的流程就是一个线程。
线程被看作是一个轻量级的继承,因为使用线程和进程比较类似,而且使用线程对于系统资源,如内存,CPU等占用要比进程小很多(线程就是进程的一部分),也就是更小的系统开销。
进程和线程的关系?
进程:可以看作是现实生活当中的公司
线程:可以看作是公司当中的每个员工
多线程的实现
东西有点多,为了方便找索引,所以用了一级标题,还是归属在线程下的喔
创建线程的三种方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
例子分别如下:
继承Thread类实现Runnable接口1
2
3
4
5
6public class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}实现Callable接口1
2
3
4
5
6public class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable is running");
}
}1
2
3
4
5
6
7public class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
System.out.println("Callable is running");
return 100;
}
}Thread
java使用java.lang.Thread
类代表线程,所有的线程都必须是Thread类或其子类的实例。
通过继承Thread类来创建并启动多线程的步骤如下:
1.定义类继承Thread类,并重写该类run()方法,run()方法体就是代表了线程需要完成的任务,称为线程执行体。
2.创建Thread子类的实例,创建线程对象。
3.调用线程对象的start()方法来启动多线程。
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.iweb.test;
public class MyThread extends Thread {
public void run() {
for (int i=0;i<=10;i++){
// 获得当前线程的名字
// Thread.currentThread()获得当前线程
// 当前线程的名字
System.out.println(Thread.currentThread().getName()+"正在执行--->"+i);
try {
// 休眠的方法
Thread.sleep(1000); // 1000毫秒/1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package com.iweb.test;
public class TestThread {
public static void main(String[] args) {
//创建线程对象
MyThread t1=new MyThread();
// 开启线程
t1.start();
//在main方法中执行for循环
for(int i=0;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"正在执行:"+i);
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
从运行结果看main线程和自定义线程运行会交运行,java程序启动时实际上启动了一个JVM线程,在这个JVM线程里先启动主线程main()方法(主线程mainThread),然后在main方法中创建和启动了其他线程(通过继承Thread类创建的线程称为子线程)。
线程启动
使用start()方法,线程进入Runnable可运行状态,它将向线程调度器注册这个线程,这意味着它可以由JVM调度并执行;但是这并不意味线程就会立即运行,只有当CPU分配时间片,这个获得了时间片时,才可以执行run()方法,start()方法去调用run(),而run方法则是需要去重写的,包含了线程的主体(真正的逻辑)。
线程名字
上面的运行代码中发现运行结果,没有给名字,会有默认的名字,可以自己指定,通过构造方法:
public Thread(String name):分配新的Thread对象。
public final void setName(String name):改变线程名称,使之与参数 name 相同。
两种方式给线程取名字:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package com.iweb.test;
public class MyThread extends Thread {
// 定义指定线程名称的构造函数
public MyThread(String name){
// 调用父类的String参数的构造方法,指定线程的名称
super(name);
}
public void run() {
for (int i=0;i<=10;i++){
// 获得当前线程的名字
// Thread.currentThread()获得当前线程
// getName()当前线程的名字
System.out.println(Thread.currentThread().getName()+"正在执行--->"+i);
try {
// 休眠的方法
Thread.sleep(1000); // 1000毫秒/1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
对应测试1
2
3
4
5
6
7
8
9
10
11
12
13 package com.iweb.test;
public class TestThread {
public static void main(String[] args) {
//创建线程对象
// 1.构造方法
MyThread t1=new MyThread("子线程1");
// 2.setName给线程取名字
t1.setName("子线程2");
// 开启线程
t1.start();
}
}
Runnable
实现线程可以继承于Thread类,也可以通过java.lang.Runnable
接口进行实现,定义其中唯一的run()方法,也可以创建一个线程。
步骤:
1.定义Runnable接口的实现类,并重写该接口的run()方法。
2.创建Runnable 实现类的实例,并以此实例作为Thread的targer来创建Thread对象,该Thread对象才是真正的线程对象。
3.调用线程对象的start()方法来启动线程。
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.iweb.test;
// 这并不是一个线程,是一个任务对象,它还不是一个线程
public class MyRun implements Runnable {
public void run() {
for (int i=0;i<=10;i++){
// 获得当前线程的名字
// Thread.currentThread()获得当前线程
// getName()当前线程的名字
System.out.println(Thread.currentThread().getName()+"正在执行--->"+i);
try {
// 休眠的方法
Thread.sleep(1000); // 1000毫秒/1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package com.iweb.test;
import sun.awt.windows.ThemeReader;
public class TestRun {
public static void main(String[] args) {
// 方式一
MyRun myRun=new MyRun();
// 创建Thread线程对象
Thread t1=new Thread(myRun);
t1.setName("t1");
t1.start();
// 方式二
// 使用匿名内部类
new Thread(new Runnable() {
public void run() {
for (int i=0;i<=10;i++){
// 获得当前线程的名字
// Thread.currentThread()获得当前线程
// getName()当前线程的名字
System.out.println(Thread.currentThread().getName()+"正在执行--->"+i);
try {
// 休眠的方法
Thread.sleep(1000); // 1000毫秒/1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
对比以上两种方法,有以下区别
Thread是类,而runnable是接口,Thread本身是实现了runnable接口的类,我们知道一个类只能有一个父类,但是却能实现多个接口,因此runnable具有更好的扩展性。
线程的生命周期
由死到生还是由生到死?这是个哲学问题
它要经历多个状态,按顺序来是:
- 新建状态(New):使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获取了CPU,执行run()方法。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时5. 死亡状态(Dead):线程执行完毕或者被别的线程杀死,就会进入死亡状态。
阻塞的情况分三种: - 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
线程调度
多线程的并发(同一个时间段内处理多个任务)运行是指线程轮流获得CPU的使用权,分别执行各自的任务。在线程的可运行池中,会有多个处于就绪状态的线程等待CPU,Java虚拟机的一项任务就是负责线程的调度,线程的调度是按照特定的机制为多个线程分配CPU的使用权
有两种调度模型:
- 分时调度
- 抢占式调度
分时调度(鱼露均占):是指所有线程轮流使用CPU的使用权,且平均分配每个线程占用CPU的时间。
抢占式调度(强者为王):优先让可运行池中优先级别最高的线程占用CPU,如果优先级别一样,就随机选一个,使用占用CPU,然后线程运行,直到放弃CPU。
注意:
- 分时调度是所有线程轮流使用CPU的使用权,且平均分配每个线程占用CPU的时间。
- 抢占式调度是优先让可运行池中优先级别最高的线程占用CPU,如果优先级别一样,就随机选一个,使用占用CPU,然后线程运行,直到放弃CPU。
线程的优先级
在多线程系统中,线程优先级决定了线程在抢占CPU时间时的调度顺序,但在大多数情况下,较高优先级别线程更容器获得CPU时间,从而提供响应速度。
线程优先级API
- public final int getPriority():返回线程优先级
- public final void setPriority(int newPriority):更改线程优先级
优先级别用整数表示,取值范围1-10,Thread类有三个静态常量。这里强调的是,优先级别越高抢占CPU的概率越高,只是说概率高,并不是优先级别高就肯定比优先级别低的要先抢占CPU。
表格展示:(静态常量+说明)
| 静态常量 | 说明 |
| —- | —- |
| MAX_PRIORITY | 最大优先级,值为10 |
| MIN_PRIORITY | 最小优先级,值为1 |
| NORM_PRIORITY | 默认优先级,值为5 |1
2
3
4
5
6
7
8
9
10
11
12public class TestRun {
public static void main(String[] args) {
// 创建Thread线程对象
Thread t1=new Thread(new MyRun(),"min");
// 设置优先级别
t1.setPriority(Thread.MIN_PRIORITY);
t1.start();
Thread t2=new Thread(new MyRun(),"max");
t2.setPriority(Thread.MAX_PRIORITY);
t2.start();
}
}
根据时间片轮询调度,所以并发执行。
yield()&join()
- public static void yield():暂停当前线程,让出CPU资源,给其他线程执行机会。
- public final void join():等待线程死亡。
案例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package com.iweb.test1;
// 烧水
public class BoilWater implements Runnable{
public void run() {
System.out.println("开始烧水..");
// 烧水需要时间,有个过程
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("水烧开了..");
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package com.iweb.test1;
public class WashingCups implements Runnable {
public void run() {
System.out.println("开始洗杯子..");
for(int i=1;i<=5;i++){
System.out.println("洗第"+i+"个杯子");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("杯子洗完了..");
}
}
测试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package com.iweb.test1;
import sun.awt.windows.ThemeReader;
public class Test1 {
public static void main(String[] args) {
System.out.println("---------------欢迎来家里做客---------------");
System.out.println("准备给您泡茶!!");
Thread t1=new Thread(new BoilWater());
Thread t2=new Thread(new WashingCups());
// 启动烧开水和洗杯子的线程
t1.start();
t2.start();
System.out.println("茶泡好了~~");
System.out.println("请慢用~~");
}
}
通过案例可以发现:
应该是两个子线程执行的时候,我的主线程应该要等等,等两个子线程先把事情做完,再执行。主线程调用yieId()方法,让它暂时放弃CPU,给其他线程一个运行机会,但是当前线程仍然处于就绪状态。
1 | package com.iweb.test1; |
通过案例发现,并没有什么区别?主要并没有让出CPU执行权给子线程去执行,这是什么原因?Thread.yield()
用于当前线程愿意放弃CPU使用权,但是当前线程仍然处于就绪状态,线程调度器再次调用的时候,也还有可能继续运行这个线程。
public final void join():将一个线程合并到当前线程中,当前线程受阻塞,加入的线程执行直到结束。
使用Thread.join()
后,即这个子线程会获得执行权,主线程会等子线程执行完后再执行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package com.iweb.test1;
import sun.awt.windows.ThemeReader;
public class Test1 {
public static void main(String[] args) {
System.out.println("---------------欢迎来家里做客---------------");
System.out.println("准备给您泡茶!!");
// 主线程放弃CPU使用权
//Thread.yield();
Thread t1=new Thread(new BoilWater());
Thread t2=new Thread(new WashingCups());
// 启动烧开水和洗杯子的线程
t1.start();
t2.start();
try {
// 先烧水,再洗杯子
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("茶泡好了~~");
System.out.println("请慢用~~");
}
}
线程同步
线程同步是指多个线程按照一定的顺序执行,一个线程执行完后,下一个线程才能执行。
以买票为情景:
需求:使用多线程实现三个窗口卖票业务1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package com.iweb.test1;
public class Ticket implements Runnable {
// 卖100张票
private int ticket=100;
public void run() {
while(true){
if(ticket>0){ // 有票可以卖
// 模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在卖:"+ticket--);
}
}
}
}
测试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package com.iweb.test1;
public class Test2 {
public static void main(String[] args) {
// 创建线程任务对象
Ticket ticket=new Ticket();
// 创建三个窗口
Thread t1=new Thread(ticket,"窗口1");
Thread t2=new Thread(ticket,"窗口2");
Thread t3=new Thread(ticket,"窗口3");
// 同时卖票
t1.start();
t2.start();
t3.start();
}
}
分析
当多个线程同时访问共享资源时,可能会导致数据不一致等线程安全问题。Java提供了synchronized关键字来实现线程同步,确保同一时刻只有一个线程可以访问共享资源。
运行中出现了一张票被多个窗口卖了,出现这个问题的原因是线程执行时是有随机性的,当一个线程休眠时,其他的线程就可以抢到CPU了,休眠之后就又可以抢占CPU,此时如果一个线程刚好执行到了ticket—,还没有来得及打印,其他线程抢回了CPU,并且执行了ticket—,这时可能出现以上的情况。这种问题,几个窗口(线程)票数不同步了,这种问题成为线程不安全。
解决方法
在线程使用一个资源的时候加锁即可。当第一个线程访问的时候会加锁,其他线程就不能使用这个资源,直到前面的线程释放锁。
我简单叙述下这个案例的发生的流程:
- 窗口1线程访问资源,发现没有锁,就会加锁。
- 窗口2线程访问资源,发现有锁,就会等待。
- 窗口1线程使用资源,使用完毕后会释放锁。
- 窗口2线程获得锁,使用资源。
- 如此往复,直到资源被使用完毕。
同步代码块
通过关键字synchronized
来实现,就相当于给代码加锁了,括号中需要传入一个锁对象,可以任意的,但必须是唯一的,通常会使用Thread.class作为锁对象,因此字节码对象是唯一的。
1 | package com.iweb.test1; |
测试:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package com.iweb.test1;
public class Test2 {
public static void main(String[] args) {
// 创建线程任务对象
Ticket ticket=new Ticket();
// 创建三个窗口
Thread t1=new Thread(ticket,"窗口1");
Thread t2=new Thread(ticket,"窗口2");
Thread t3=new Thread(ticket,"窗口3");
// 同时卖票
t1.start();
t2.start();
t3.start();
}
}
同步方法
把synchronized加在方法上就是同步方法,同步方法就是锁住方法里面的所有代码,这个同步方法锁的当前对象,也就是this对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.iweb.test1;
public class Ticket implements Runnable {
// 卖100张票
private int ticket=100;
public void run() {
while(true){
func();
}
}
// 同步方法
public synchronized void func(){
if(ticket>0){ // 有票可以卖
// 模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(Thread.currentThread().getName()+"正在卖:"+ticket--);
}
}
}
锁
上面的同步代码块和同步方法,虽然也是起到了把一段代码加锁效果,但是并没有直接看出究竟在哪里加了锁,哪里释放了锁。为了更清晰的表达如何加锁和释放锁,JDK提供了一个新的锁对象Lock,Lock实现提供比synchronized方法和语句可以获得更广泛的锁定操作Lock也提供了获取锁和释放锁的方法。
这里锁所涉及的部分可以理解为被
lock.lock(); // 加锁
和lock.unlock(); // 释放锁
所包含的部分的代码被上锁了,只能被一个资源访问
1 | package com.iweb.test1; |
死锁
指的是两个或多个线程在执行过程中,由于竞争资源而造成的一种阻塞现象,若外力作用,这些线程都将无法向前推进,死锁通常发生在两个或多个线程互相等待对象释放锁的情况,一般就是在锁的嵌套中容易发生,所以要避免这种写法。
说人话就是A拿着B房间的钥匙,B拿着A房间的钥匙,谁都不松手谁都进不去,形成了一种僵直状态
解决死锁问题
这个状态也很好解决,但凡有任何一方先释放资源即可
只有两种方式:
- 让当前拥有a对象的线程先等待,让出a锁
- 让当前拥有b对象的线程先等待,让出b锁
等待唤醒机制 - public final void wait():当前线程等待,直到被其他线程唤醒
- public final void notify():唤醒单个线程
- public final void notifyAll():唤醒所有线程
等待(wait):当一个线程执行到某个对象的wait()方法时,它会释放当前持有的锁(如果有的话),并进入等待状态。此时线程不在参与CPU的调用,直到其他线程调用同一对象的notify¬ifyAll方法将其唤醒。
唤醒(notify¬ifyAll):唤醒在等待的某个线程,如果有多个线程在等待,那么具体唤醒哪一个是随机的。
notifyAll:调用wait()方法的线程会释放其持有的锁,被唤醒的线程在执行之前,必须重新获取被释放的锁。
MyThread让出了a对象锁,MyThread1就可以执行完毕。MyThread进入等待状态,直到其他线程调用同一对象的notify¬ifyAll方法将其唤醒。
以下是个小示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46package com.iweb.test1;
public class Test3 {
public static void main(String[] args) {
// 创建一个锁对象
Object lock=new Object();
// 创建线程任务对象
Runnable runnable=new Runnable() {
public void run() {
synchronized (lock){
System.out.println(Thread.currentThread().getName()+"开始等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"等待结束");
}
}
};
// 创建线程对象
Thread t1=new Thread(runnable,"线程1");
Thread t2=new Thread(runnable,"线程2");
// 启动线程
t1.start();
t2.start();
// 唤醒线程1
t1.notify();
// 唤醒线程2
t2.notify();
// 等待线程1结束
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 等待线程2结束
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("程序结束");
}
}
线程池
在之前我们写的代码中,用到线程就创建,用完之后线程就消失了,这样会浪费操作系统的资源,也存在一些弊端,通过线程池就可以解决这个问题。
线程池是一种线程使用模式,它维护着多个线程,等待监督管理者分配可并发执行的任务。
线程池的原理
创建一个空的线程池(容器),在没有任务时线程处于空闲状态,当请求到来;线程池给这个请求分配了一个空闲的线程,任务完成后回到线程池等待下此任务(不是销毁),这样就可以实现线程的重用。
线程池的使用
在java中,Executors提供了很多静态工厂方法来创建不同类型的线程池,学习线程池,Executors不可或缺的。
PS:
public static ExecutorService newFixedThreadPool(int nThreads):用于创建一个固定大小的线程池,固定大小的线程池就是线程池中始终保持一定数据的线程,这些线程可以重复使用,执行多个任务。
public static ExecutorService newCachedThreadPool():用于创建一个可缓存的线程池,可缓存的线程池就是线程池中的线程数量不确定,当有任务需要执行时,会创建新的线程,当没有任务需要执行时,会回收空闲的线程。
public static ExecutorService newSingleThreadExecutor():用于创建一个单线程的线程池,单线程的线程池就是线程池中始终保持一个线程,这个线程可以重复使用,执行多个任务。
1 | package com.iweb.test1; |
从以上代码可以看出:
- 线程池的submit方法和execute方法都是用来执行任务的,区别在于submit方法可以返回一个Future对象,Future对象可以用来获取任务的执行结果,而execute方法没有这个功能。
- 线程池的submit方法和execute方法都可以用来执行任务,但是submit方法可以用来执行Callable任务,而execute方法只能用来执行Runnable任务。
- 线程池的submit方法和execute方法都可以用来执行任务,但是submit方法可以用来执行Callable任务,而execute方法只能用来执行Runnable任务。
施工中—————-